package com.devehx.wallet.comm;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.novacrypto.bip39.MnemonicGenerator;
import io.github.novacrypto.bip39.Words;
import io.github.novacrypto.bip39.wordlists.English;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.crypto.*;
import org.bitcoinj.wallet.DeterministicSeed;
import org.springframework.util.StringUtils;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.Wallet;
import org.web3j.crypto.*;
import org.web3j.protocol.ObjectMapperFactory;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.core.methods.response.*;
import org.web3j.protocol.http.HttpService;
import org.web3j.utils.Numeric;

import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.ExecutionException;

/**
 * anther : hux
 * datetime :
 * description :
 */
@Slf4j
public class WalletKit {

    public static Integer web3Idx = 0;
    public static final List<Web3j> web3jList = new ArrayList<>();

    public static final String nodeUrl;

    private static final SecureRandom secureRandom = new SecureRandom();

    /**
     * 通用的以太坊基于bip44协议的助记词路径 （imtoken jaxx Metamask myetherwallet）
     */
    private final static String ETHTYPE = "m/44'/60'/0'/0/0";

    static {
        ResourceBundle application = ResourceBundle.getBundle("application");
        String[] nodeArr = application.getString("eth.url.node").split(",");
        nodeUrl = nodeArr[0];
        for (String nodeUrl : nodeArr) {
            web3jList.add(Web3j.build(new HttpService(nodeUrl)));
        }
    }

    /**
     * 轮询获取web3连接
     */
    public static Web3j getWeb3j() {
        if (web3Idx++ >= web3jList.size() - 1) {
            web3Idx = 0;
        }
        return web3jList.get(web3Idx);
    }

    public static Web3j getWeb3j2() {
        return Web3j.build(new HttpService(nodeUrl));
    }

    /**
     * 生成一组随机的助记词
     *
     * @return 助记词
     */
    public static String generateMnemonics() {
        StringBuilder sb = new StringBuilder();
        byte[] entropy = new byte[Words.TWELVE.byteLength()];
        new SecureRandom().nextBytes(entropy);
        new MnemonicGenerator(English.INSTANCE).createMnemonic(entropy, sb::append);
        log.info(sb.toString());
        return sb.toString();
    }

    /**
     * 获取最新的区块号码
     */
    public static BigInteger getBlockNumber() {
        try {
            return getWeb3j().ethBlockNumber().send().getBlockNumber();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取nonce
     *
     * @param address 钱包地址
     * @return nonce
     */
    public static BigInteger getNonce(String address) {
        EthGetTransactionCount ethGetTransactionCount;
        try {
            ethGetTransactionCount = getWeb3j().ethGetTransactionCount(address, DefaultBlockParameterName.LATEST).sendAsync().get();
        } catch (Exception e) {
            log.error("获取{}的nonce失败", address, e);
            return null;
        }
        return ethGetTransactionCount.getTransactionCount();
    }


    /**
     * 导入助记词获取钱包
     *
     * @param mnemonic 助记词
     * @param password 密码
     */
    public static Account importMnemonic(String mnemonic, String password) {
        return importMnemonic(Arrays.asList(mnemonic.split(Const.STR_SPACE)), password);
    }

    /**
     * 导入助记词获取钱包
     *
     * @param mnemonic 助记词
     * @param password 密码
     */
    public static Account importMnemonic(List<String> mnemonic, String password) {
        return importMnemonicList(mnemonic, password);
    }

    /**
     * 通过助记词得到私玥
     *
     * @param mnemonic 私玥
     */
    public static String getPrivateKey(String mnemonic) {
        //使用密码和助记词让账户解锁
        Account ethHDWallet = WalletKit.importMnemonic(mnemonic, Const.STR_BLANK);
        if (ethHDWallet == null) {
            return Const.STR_BLANK;
        }
        //返回私钥
        return ethHDWallet.getPrivateKey();
    }


    /**
     * @param mnemonic 助记词
     * @param password 密码
     */
    private static Account importMnemonicList(List<String> mnemonic, String password) {
        String[] pathArray = ETHTYPE.split("/");
        //密码短语
        String passphrase = Const.STR_BLANK;
        long creationTimeSeconds = System.currentTimeMillis() / 1000;
        //使用用户助记词构建
        DeterministicSeed ds = new DeterministicSeed(mnemonic, null, passphrase, creationTimeSeconds);
        return createEthWallet(ds, pathArray, password);
    }

    /**
     * 创建钱包
     *
     * @param ds        DeterministicSeed
     * @param pathArray 以太坊基于bip44协议的助记词路径
     * @param password  密码
     */
    private static Account createEthWallet(DeterministicSeed ds, String[] pathArray, String password) {
        //根私钥
        byte[] seedBytes = ds.getSeedBytes();
        //助记词
        List<String> mnemonic = ds.getMnemonicCode();
        //助记词必须是12个
        if (Objects.isNull(mnemonic) || mnemonic.size() != 12) {
            log.error("助记词格式不正确");
            return null;
        }
        try {
            //助记词种子
            byte[] mnemonicSeedBytes = MnemonicCode.INSTANCE.toEntropy(mnemonic);
            ECKeyPair mnemonicKeyPair = ECKeyPair.create(mnemonicSeedBytes);
            WalletFile walletFile = org.web3j.crypto.Wallet.createLight(password, mnemonicKeyPair);
            ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper();
            //存这个keystore 用完后删除
            String keystore = objectMapper.writeValueAsString(walletFile);
            //验证
            WalletFile checkWalletFile = objectMapper.readValue(keystore, WalletFile.class);
            ECKeyPair ecKeyPair = org.web3j.crypto.Wallet.decrypt(password, checkWalletFile);
            byte[] checkMnemonicSeedBytes = Numeric.hexStringToByteArray(ecKeyPair.getPrivateKey().toString(16));
//            System.out.println("验证助记词种子 "
//                    + Arrays.toString(checkMnemonicSeedBytes));
            List<String> checkMnemonic = MnemonicCode.INSTANCE.toMnemonic(checkMnemonicSeedBytes);
            System.out.println("验证助记词 " + Arrays.toString(checkMnemonic.toArray()));
        } catch (MnemonicException.MnemonicLengthException | MnemonicException.MnemonicWordException | MnemonicException.MnemonicChecksumException | CipherException | IOException e) {
            e.printStackTrace();
        }
        if (seedBytes == null) {
            return null;
        }
        DeterministicKey dkKey = HDKeyDerivation.createMasterPrivateKey(seedBytes);
        for (int i = 1; i < pathArray.length; i++) {
            ChildNumber childNumber;
            if (pathArray[i].endsWith("'")) {
                int number = Integer.parseInt(pathArray[i].substring(0,
                        pathArray[i].length() - 1));
                childNumber = new ChildNumber(number, true);
            } else {
                int number = Integer.parseInt(pathArray[i]);
                childNumber = new ChildNumber(number, false);
            }
            dkKey = HDKeyDerivation.deriveChildKey(dkKey, childNumber);
        }

        ECKeyPair keyPair = ECKeyPair.create(dkKey.getPrivKeyBytes());
        try {
            WalletFile walletFile = Wallet.createLight(password, keyPair);
            ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper();
            // 保存
            String keyStore = objectMapper.writeValueAsString(walletFile);
            Account ethHDWallet = new Account();
            ethHDWallet.setPrivateKey(keyPair.getPrivateKey().toString(16));
            ethHDWallet.setPublicKey(keyPair.getPublicKey().toString(16));
            ethHDWallet.setAddress("0x" + walletFile.getAddress());
            ethHDWallet.setMnemonic(mnemonic);
            //ethHDWallet.setKeystore(keyStore);
            log.info("EthWallet:" + ethHDWallet);
            return ethHDWallet;
        } catch (CipherException | JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 创建一个以太坊账户
     */
    public static Account generateWallet() {
        return generateMnemonic(ETHTYPE, Const.STR_BLANK);
    }

    /**
     * 生成钱包
     *
     * @param path     通用的以太坊基于bip44协议的助记词路径
     * @param password 密码
     */
    public static Account generateMnemonic(String path, String password) {
        if (!path.startsWith("m") && !path.startsWith("M")) {
            //参数非法
            throw new RuntimeException("参数非法");
        }
        String[] pathArray = path.split("/");
        if (pathArray.length <= 1) {
            //内容不对
            throw new RuntimeException("内容错误");
        }
        String passphrase = "";
        long creationTimeSeconds = System.currentTimeMillis() / 1000;
        DeterministicSeed ds = new DeterministicSeed(secureRandom, 128, passphrase, creationTimeSeconds);
        return createEthWallet(ds, pathArray, password);
    }


    /**
     * 根据交易哈希查询交易状态
     *
     * @param txHash 交易哈希
     * @return 交易状态枚举
     */
    public static TxState queryTx(String txHash) {
        try {
            EthGetTransactionReceipt send = getWeb3j().ethGetTransactionReceipt(txHash).send();
            Optional<TransactionReceipt> trOp = send.getTransactionReceipt();
            if (!trOp.isPresent()) {
                log.info("交易哈希不正确或者，交易还在等待确认，交易哈希：{}", txHash);
                return TxState.PENDING;
            }
            TransactionReceipt transactionReceipt = trOp.get();
            //转账状态
            final String status = transactionReceipt.getStatus();
            //判断以太坊的状态
            switch (status) {
                case "0x0":
                    log.debug("交易失败，交易哈希：{}", txHash);
                    return TxState.FAIL;
                case "0x1":
                    //交易所在的区块
                    final BigInteger txBlockNumber = transactionReceipt.getBlockNumber();
                    //最新的区块号码
                    final BigInteger latestBlockNum = getWeb3j().ethBlockNumber().send().getBlockNumber();
                    //得到交易确认数（ 最新区块号-交易的区块号+1=交易确认数）
                    final int confirmNum = latestBlockNum.subtract(txBlockNumber).add(BigInteger.ONE).intValue();
                    /*
                    比特币一般要6个区块确认才算成功，
                    以太坊的一般12个区块确认了以后才算交易成功，这里如果确认的区块数量是小于12的话就提示正在确认
                     */
                    if (confirmNum >= 12) {
                        log.debug("交易还已经完成，区块确认数量：{}，交易哈希：{}", confirmNum, txHash);
                        return TxState.CONFIRMED;
                    } else {
                        log.debug("交易还在确认中，区块确认数量：{}，交易哈希：{}", confirmNum, txHash);
                        return TxState.CONFIRMING;
                    }
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("查询交易状态出错，交易哈希：{}", txHash);
        }
        return TxState.ERR;
    }

    /**
     * 获取以太币余额
     *
     * @param address 钱包地址
     * @return 余额（wei单位）
     */
    public static BigInteger getBalance(String address) {
        try {
            //获取指定钱包的以太币余额
            return getWeb3j().ethGetBalance(address, DefaultBlockParameterName.LATEST).send().getBalance();
        } catch (IOException e) {
            e.printStackTrace();
            //发生异常就返回0
            return BigInteger.ZERO;
        }
    }

    /**
     * 获取ERC20代币的余额
     *
     * @param walletAddress   钱包地址
     * @param contractAddress 合约地址
     * @return 余额（wei单位）
     */
    public static BigInteger getBalance(String walletAddress, String contractAddress) {
        try {
            if (Objects.isNull(contractAddress) || Objects.equals("eth", contractAddress)) {
                return getBalance(walletAddress);
            }
            final String GET_BALANCE_PREFIX = "0x70a08231000000000000000000000000";
            Transaction ethCallTransaction = Transaction.createEthCallTransaction(
                    walletAddress, contractAddress, GET_BALANCE_PREFIX + walletAddress.substring(2)
            );
            String value = getWeb3j().ethCall(ethCallTransaction, DefaultBlockParameterName.PENDING).send().getValue();
            return new BigInteger(value.substring(2), 16);
        } catch (Exception e) {
            return BigInteger.ZERO;
        }
    }

    /**
     * 获取最新的GasPrice
     */
    public static BigInteger getGasPrice() {
        try {
            return getWeb3j().ethGasPrice().send().getGasPrice();
        } catch (IOException e) {
            log.error("获取gas price失败");
            return null;
        }
    }

    /**
     * 以太坊转账
     *
     * @param contract 合约地址
     * @param from     转账地址
     * @param key      转账地址的私钥
     * @param to       收款地址
     * @param num      转账数量
     */
    public static String transfer(String contract, String from, String key, String to, BigInteger num) {
        BigInteger gasPrice = getGasPrice();
        if (Objects.isNull(gasPrice)) {
            return null;
        }
        BigInteger gasLimit = BigInteger.valueOf(60000L);
        //合约地址是空或者合约地址是‘eth’就是以太币转账，否则就是代币转账
        if (StringUtils.isEmpty(contract) || Const.MAIN_NAME.equalsIgnoreCase(contract)) {
            return transferEth(from, key, to, num, gasPrice, gasLimit);
        } else {
            return transferErc20(from, key, to, contract, num, gasPrice, gasLimit);
        }
    }

    /**
     * 以太坊转账
     *
     * @param from     转账地址
     * @param key      转账地址的私钥
     * @param to       收款地址
     * @param num      转账数量
     * @param gasPrice gasPrice
     * @param gasLimit gasLimit
     */
    public static String transferEth(String from, String key, String to, BigInteger num, BigInteger gasPrice, BigInteger gasLimit) {
        try {
            //用私钥加载转账凭证
            Credentials credentials = Credentials.create(key);
            //nonce,交易笔数
            BigInteger nonce = getNonce(from);
            //创建ether交易对象
            RawTransaction etherTransaction = RawTransaction.createEtherTransaction(nonce, gasPrice, gasLimit, to, num);
            //交易签名
            byte[] bytes = TransactionEncoder.signMessage(etherTransaction, credentials);
            String hexValue = Numeric.toHexString(bytes);
            //发送交易
            EthSendTransaction transaction = getWeb3j().ethSendRawTransaction(hexValue).sendAsync().get();
            //判断转账是否有错误
            if (transaction.hasError()) {
                log.error("转账失败,错误信息：{}", transaction.getError().getMessage());
                return transaction.getError().getMessage();
            }
            return transaction.getTransactionHash();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * ERC20代币转账
     *
     * @param from            转账地址
     * @param key             转账地址的私钥
     * @param to              收款地址
     * @param contractAddress 代币合约地址
     * @param num             转账数量
     * @param gasPrice        gasPrice
     * @param gasLimit        gasLimit
     * @return 如果转账成功的话就是交易hash。
     */
    public static String transferErc20(String from, String key, String to, String contractAddress, BigInteger num, BigInteger gasPrice, BigInteger gasLimit) {
        //用私钥加载转账凭证
        Credentials credentials = Credentials.create(key);
        try {
            //获取nonce(交易的笔数)
            BigInteger nonce = getWeb3j().ethGetTransactionCount(from, DefaultBlockParameterName.PENDING).send().getTransactionCount();
            //ERC20代币合约方法
            Function transfer = new Function("transfer",
                    Arrays.asList(new Address(to), new Uint256(num)),
                    Collections.singletonList(new TypeReference<Type>() {
                    }));
            //创建交易对象
            RawTransaction transaction = RawTransaction.createTransaction(nonce, gasPrice, gasLimit, contractAddress, FunctionEncoder.encode(transfer));
            //签名交易
            String hexValue = Numeric.toHexString(TransactionEncoder.signMessage(transaction, credentials));
            //发送交易
            EthSendTransaction ethSendTransaction = getWeb3j().ethSendRawTransaction(hexValue).sendAsync().get();
            //判断转账是否有错误
            if (ethSendTransaction.hasError()) {
                log.error("转账失败,错误信息：{}", ethSendTransaction.getError().getMessage());
                return ethSendTransaction.getError().getMessage();
            }
            //返回交易哈希
            return ethSendTransaction.getTransactionHash();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据区块号码获取区块信息
     *
     * @param number 区块号码
     */
    public static EthBlock.Block getBlockByNumber(int number) {
        //根据区块号获取区块数据
        DefaultBlockParameter value = DefaultBlockParameter.valueOf(BigInteger.valueOf(number));
        try {
            Web3j web3j2 = getWeb3j();
            return web3j2.ethGetBlockByNumber(value, true).send().getBlock();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }


    public static boolean isTxHash(String str) {
        return !Objects.isNull(str) && str.startsWith("0x") && str.length() == 66;
    }

}